Skip to content

fix(app-shell): an edit a Studio pillar holds unsent guards departure like a dirty pillar - #26839

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-qt6zec
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-qt6zec

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread

Fixes #26705

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:Studio 把「暂存未发的编辑」(还不能保存的字段、步骤、组件)也算作未保存工作;离开支柱、刷新页面、在左侧栏切换到别的对象/流程/页面时,会先弹出与 Access 相同的确认框。
  • 为什么改:之前 Data、Automations、Interfaces 支柱暂存的编辑只拦发布,不拦离开,一次误点就静默丢失;分诊裁决是「confirm 加 beforeunload,共用一个守卫」。
  • 风险与回滚:只多问一句,不改保存与发布逻辑;暂存与否沿用已有判定。有问题直接 revert 本 PR。
  • 先看:StudioDesignSurface.tsx 里的 confirmDropUnsavedWork 和 unsavedPillarWork,然后三处左侧栏 onClick / onPick 的守卫行,最后 StudioDesignSurface.heldDataLeave-26705.test.tsx。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/studio-design/StudioDesignSurface.heldDataLeave-26705.test.tsx,预期 4 个测试全绿。
  • 需要你做的:无,席位落地。

What changes

Base 607d3bb1, then origin/main 55b5042e merged in (no conflict; nothing it brought touches these files). Then d33bfcf7: the zh catalogue preload in two metadata-admin suites (test-only, scope widened by the seat, Claim 6452362760). One shared guard, read by every departure that drops a pillar's buffer:

  • confirmDropUnsavedWork(unsaved, locale), a module function beside useHeldEditReport: the one question (engine.edit.unsavedLeaveConfirm, the text Access already asks) or true when nothing would be lost. Access's own confirmDiscardEdits now calls it too, so the question has one spelling.
  • Surface: heldEdit moves up beside pillarDirty, and unsavedPillarWork = pillarDirty || heldEdit !== null replaces pillarDirty in confirmLeavePillar, the beforeunload effect and useRegisterUnsavedEdits. That covers the pillar links (the shell seam's link guard asks while registered), Home, the package switcher, the cross-pillar ?surface= request, Create app, the shell's own departures and reload. Pillars already take their held report back on unmount, so a confirmed leave clears the guard.
  • Rail item switch (a pillar-local path the surface cannot see; another item's load replaces the one buffer the held edit lives in): the Data object rail asks on objHeld, the Automations flow rail on flowHeld, the Interfaces nav rail on leafHeld. Re-picking the open item (same object, same flow, an entry for the same leaf) drops nothing and asks nothing, mirroring Access's rail.
  • Unchanged by design: how a held edit is judged and saved, and Publish's refusal while one is held.

Why

Triage first grade on the card, verbatim: "Guard departure the way the Access pillar and the Interfaces navigation already do (confirm plus beforeunload), with one shared guard." A held edit lives only in the pillar's buffer, so before this change one stray click dropped a half-added field, step or widget that the strip had just promised was "kept here".

Tests

Three new files, one per pillar, each through the real StudioDesignSurface on the real route shape (the held edit is produced the way an author does it: a Picklist with no option, a Notify step with no Title, a Metric widget with no dataset):

  • StudioDesignSurface.heldDataLeave-26705.test.tsx, StudioDesignSurface.heldFlowLeave-26705.test.tsx, StudioDesignSurface.heldWidgetLeave-26705.test.tsx, four cases each: leaving the pillar asks (Cancel keeps the held edit, OK leaves, and walking back asks nothing); the rail switch asks (Cancel keeps it, re-picking the open item asks nothing, OK opens the other item and the hold is gone); a reload asks only once the edit is held; and the CONTROL: with nothing held, the rail, the reload and the pillar link never ask.
Runs
  • New files: 3 files, 12/12 passed (lock VERDICT command-exit 0).
  • pnpm --dir ui exec vitest run packages/app-shell/src/views/studio-design/ packages/app-shell/src/chrome/: 244 files, 1773/1773 passed, exit 0 (pre-merge head d2e66f47).
  • After merging origin/main: the three new files plus pillarNavGuard, heldPublish-11786, heldWidgetPublish-11910, re-run on the merge head b8d23d79: 6 files, 28/28 passed, exit 0.
  • Two metadata-admin suites, ResourceEditPage.discardDraftLabel-12690 and PackageFormDialog.validationTiming, now load the zh studio catalogue at module scope (await loadStudioCatalogue('zh')), as other zh-reading suites do since the studio namespace became lazy (app-shell: a published library that is 66% Console product surface — retire the 67 zero-reference exports now, decide the split, and fold the second i18n table into @object-ui/i18n #25922). The first went red on Test UI at 0a856a7d. With the zh catalogue held back 4 s, each is red without its preload (1/3, 3/5) and green with it (3/3, 5/5).
  • pnpm --filter @object-ui/app-shell type-check: exit 0.
Ablation (one-shot, through scripts/ablation-replace.mjs; each leg restored to the HEAD blob with an empty git diff HEAD)
  • Leg A, drop the held term from the surface (unsavedPillarWork = pillarDirty): 6 of 12 red, exactly the leave and reload cases of all three pillars; the rail and control cases stay green.
  • Leg B, drop the held term from each rail guard (replace objHeld !== null / flowHeld !== null / leafHeld !== null with false): each pillar's rail-switch case red (1 of 4 per file), nothing else.

Acceptance notes

  • Not guarded, measured and left as they were: creating a new object, flow or Interfaces item while another item's edit is held opens the new item (the create has already been written), and a same-pillar ?surface= request from the pending-changes sheet opens the requested item without asking. Both drop the held edit the same way a rail switch did. The card names leaving the pillar, the rail switch and reload; these two are left for a follow-up if wanted.
  • Browser Back/Forward is still not vetoed, as the shell seam's docblock already states for every page.
  • @object-ui/app-shell is private: true, so no changeset.

Verification

Gates derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 24 families, all run, reconciled with --ran (24 run, 0 NOT-MEASURED). pnpm --dir ui exec vitest run scripts/ reported one timeout in check-vi-mock-inherit.test.ts (15 s, shared box under load) and passed 83/83 when that file was re-run alone. pnpm check:new-line-citations (ui): 0 new citations.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (Not touched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (eslint on the four files: 0 errors; StudioDesignSurface.tsx carries the same 16 warnings as on the base, the three tests none.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (Publishes nothing: @object-ui/app-shell is private.)
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (Not touched.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English, apart from the maintainer brief above.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Test UI (16/16) is red on this head (b8d23d79), and the failure is not this PR's.

  • Failing test: ui/packages/core/src/actions/__tests__/actionKeys.pin.test.ts, case "lists every key the spec ActionSchema declares": missing: ['disabledReason'].
  • Cause: feat(spec): a greyed action says why — disabledReason beside disabled #26786 (on main as 13dc413f) added ActionSchema.disabledReason to the spec but did not add it to SPEC_ACTION_KEYS in ui/packages/core/src/actions/actionKeys.ts. This PR touches neither file. The queue run for another PR (run 40558907459, Test UI (15/16)) fails the same assertion, so it reaches every Test UI run that selects the @object-ui/core pin.
  • No fix exists yet, as far as I can see. Proposed patch, one line after 'disabled', in SPEC_ACTION_KEYS: 'disabledReason',, with a comment in the style of the description row, because the inventory restates the spec's declared keys and ActionDef does not need to change.
  • I am not widening this PR with it. I will re-run the failed job once when a fix is on main and merge main here if needed.

Generated by Claude Code

…n suites

ResourceEditPage.discardDraftLabel-12690 and PackageFormDialog.validationTiming
read studio-namespace rows in zh synchronously; since the namespace's zh
catalogue loads on demand (objectstack#25922) they raced the fetch on main CI.
Load it at module scope, as the other zh-reading suites do.

Claude-Session: https://claude.ai/code/session_01JQwxbZJiPgbGWj5B2jNkYx
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · seat domain:ui#1 · session_01JQwxbZeBv2y91towxasSUT · head d33bfcf7 · 2026-10-27T20:25Z

席位意见:一个共享守卫 confirmDropUnsavedWork,离开判断由 pillarDirty 扩为 pillarDirty || heldEdit;三处离开路径(顶栏离开支柱、刷新/关页、外壳入口)和三个支柱的侧栏切换都走它。保存方式与发布门控不动。

  1. Shape: Fixes #26705; 6 files: the 4 card files (+763/−18) plus two test-only preloads under the widened Claim 6452362760 (ResourceEditPage.discardDraftLabel-12690, PackageFormDialog.validationTiming, S3 leftovers; held-back zh catalogue: red without, green with); source is StudioDesignSurface.tsx +43/−18 (the heldEdit state moved up unchanged so the guard can read it; one helper; three rail onClick/onPick guards that ask only when another item opens; Access reuses the helper); three new per-pillar test files. skip-changeset (app-shell is private).
  2. Premise measured: the seat read the source diff on the head: confirmLeavePillar, the beforeunload effect and useRegisterUnsavedEdits all read unsavedPillarWork = pillarDirty || heldEdit !== null; each rail guard compares the picked item with the open one, so re-picking the open item never asks. The question text is the existing engine.edit.unsavedLeaveConfirm, with no new key. Dev: 12/12 new tests; studio-design + chrome 1773/1773; ablations red, restored.
  3. Gates / check-runs: On head d33bfcf7, 43 success · 13 skipped · 0 failed · 0 pending; mergeable_state: clean. Dev's local run: 24/24 derived gates reconciled, --ran 0 NOT-MEASURED. The scripts/ vitest family timed out once under a loaded lock and passed 83/83 alone.
  4. Accepted deviations: the report is superseded by 6452568640 (a seat-authorized second post, since an edit of 6450162175 was refused); the seat applied its body amendments. the full app-shell suite was left to CI's Test UI matrix (a local whole-suite run starved the shared lock).
  5. Out of scope: filed Studio: a held pillar edit is still dropped without asking when the author creates a new object, flow or Interfaces item, or opens another item from the Pending changes sheet #26845. Two paths still drop a held edit without asking: create while held, and opening another item from the Pending changes sheet. Skills-docs: none — UI guard only, no metadata-protocol change.
  6. Landing: not Tier H; the seat readies and arms auto-merge.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 20:26
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 27, 2026 20:26
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 942ffb2 Oct 27, 2026
57 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-qt6zec branch October 27, 2026 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants